﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using SecureCookie.Implementation.Protocol;
using SecureCookie.Implementation.Helper;
using System.Web.Configuration;

namespace SecureCookie.Implementation
{
    /// <summary>
    /// HttpModule that validates all configured incoming <see cref="HttpCookie"/> and secures all configured
    /// <see cref="HttpCookie"/> before they are sended to the client.
    /// </summary>
    public sealed class SecureCookieHttpModule : IHttpModule, IDisposable
    {
        /// <summary>
        /// See <see cref="IHttpModule.Dispose"/>
        /// </summary>
        public void Dispose() { }

        /// <summary>
        /// Attaches the event handlers required for this module.
        /// </summary>
        /// <param name="context"></param>
        public void Init(HttpApplication context)
        {
            context.BeginRequest += new EventHandler(BeginRequest);
            context.EndRequest += new EventHandler(EndRequest);
            context.PreSendRequestHeaders += new EventHandler(PreSendRequestHeaders);
        }

        /// <summary>
        /// Secure the cookies just before the response headers are sended back to the client
        /// </summary>
        /// <param name="sender"><see cref="IHttpModule"/></param>
        /// <param name="e"><see cref="IHttpModule"/></param>
        void PreSendRequestHeaders(object sender, EventArgs e)
        {
            if (HttpContext.Current != null && HttpContext.Current.Items["SecureCookieModule.AlreadyProcessed"] == null)
            {
                Hook.Factory.CreateLogger().LogVerbose(() => "Cookies will be secured by the PreSendRequestHeaders event", () => new object[] { });
                SecureCookies();
                HttpContext.Current.Items["SecureCookieModule.AlreadyProcessed"] = true;
            }
        }

        /// <summary>When the request is in its final event of the ASP.NET lifecycle and the cookies have not yet been secured, secure them</summary>
        /// <param name="sender"><see cref="IHttpModule"/></param>
        /// <param name="e"><see cref="IHttpModule"/></param>
        internal void EndRequest(object sender, EventArgs e)
        {
            if (HttpContext.Current.Items["SecureCookieModule.AlreadyProcessed"] == null)
            {
                Hook.Factory.CreateLogger().LogVerbose(() => "Cookies will be secured by the EndRequest event", () => new object[] { });
                SecureCookies();
                HttpContext.Current.Items["SecureCookieModule.AlreadyProcessed"] = true;
            }
            else
            {
                Hook.Factory.CreateLogger().LogVerbose(() => "EndRequest is doing nothing since the cookies were alread processed in the PreSendRequestHeaders event", () => new object[] { });
            }

        }
        /// <summary>
        /// Searches for all the cookies on the response that are configured to be secured and
        /// <list type="bullet">
        /// <item>Removes the original cookie from the response</item>
        /// <item>Secures the original cookie</item>
        /// <item>Places the secured cookie back on the response</item>
        /// </list>        
        /// </summary>
        private static void SecureCookies()
        {
            HigherOrderFunctions.TryAndSwallow(() =>
            {
                SecureCookieSettings.Default.CookiesToSecure.Cast<string>()
                .Select(x => HttpContext.Current.Response.Cookies.Find(x))
                .Where(x => x != null)
                .ToList()
                .Do(x => Hook.Factory.CreateLogger().LogVerbose(() => "Before securing: Going to remove cookie from response with name [{0}]", () => new object[] { x.Name }))
                .Do(x => HttpContext.Current.Response.Cookies.Remove(x.Name))
                .Do(x => Hook.Factory.CreateLogger().LogVerbose(() => "Before securing: Removed cookie from response with name [{0}]", () => new object[] { x.Name }))
                .Select(x => Protocol.SecureCookieProtocol.MakeSecure(x, Hook.Factory.CreateServerSideDataProvider().GetServerSideData(), new Protocol.Impl.RequestStateFromHttpContextItems()))
                .ForEach(x => HttpContext.Current.Response.Cookies.Set(x));
            }, (Exception ex) =>
            {
                Hook.Factory.CreateLogger().LogException(ex);
            });
        }
        /// <summary>
        /// Reads out all configured cookies from the request and validates the cookie. When the cookie is valid, it is being restored
        /// on the request with only the value. When the received cookie is invalid, it is logged and the cookie is being removed from the request.
        /// </summary>
        /// <param name="sender"><see cref="IHttpModule"/></param>
        /// <param name="e"><see cref="IHttpModule"/></param>
        internal void BeginRequest(object sender, EventArgs e)
        {
            HigherOrderFunctions.TryAndSwallow(() =>
            {
                SecureCookieSettings.Default.CookiesToSecure.Cast<string>()
                .Select(x => HttpContext.Current.Request.Cookies.Find(x))
                .Where(x => x != null)
                .ForEachTryCatch(cookie =>
                {
                    var validationResult = Protocol.SecureCookieProtocol.Validate(cookie, Hook.Factory.CreateServerSideDataProvider().GetServerSideData(), new Protocol.Impl.RequestStateFromHttpContextItems());
                    if (validationResult == ValidationResult.Valid)
                    {
                        Hook.Factory.CreateLogger().LogVerbose(() => "Cookie with name [{0}] is restored because of [{1}]", () => new object[] { cookie.Name, validationResult });
                        HttpContext.Current.Request.Cookies.Set(Protocol.SecureCookieProtocol.Restore(cookie));
                    }
                    else
                    {
                        Hook.Factory.CreateLogger().LogVerbose(() => "Cookie with name [{0}] is removed because of [{1}]", () => new object[] { cookie.Name, validationResult });
                        HttpContext.Current.Request.Cookies.Remove(cookie.Name);
                    }

                }, (cookie, exception) =>
                {
                    exception.Data.Add("CookieThatWasBeingValidated", cookie.ToDetailedString());
                    // Swallow the exception here, think about wether or not this is justified for your situation!
                    Hook.Factory.CreateLogger().LogException(exception);
                });
            },
             (Exception ex) =>
             {
                 // Swallow the exception here, think about wether or not this is justified for your situation!
                 Hook.Factory.CreateLogger().LogException(ex);
             });
        }
    }
}
